function padWithZeros (vNumber, width) {
    let numAsString = vNumber.toString();
    while (numAsString.length < width) {
        numAsString = `0${numAsString}`;
    }
    return numAsString;
}
  
function addZero (vNumber) {
    return padWithZeros(vNumber, 2);
}
  
/**
   * Formats the TimeOffset
   * Thanks to http://www.svendtofte.com/code/date_format/
   * @private
   */
function offset (timezoneOffset) {
    const os = Math.abs(timezoneOffset);
    let h = String(Math.floor(os / 60));
    let m = String(os % 60);
    if (h.length === 1) {
        h = `0${h}`;
    }
    if (m.length === 1) {
        m = `0${m}`;
    }
    return timezoneOffset < 0 ? `+${h}${m}` : `-${h}${m}`;
}
  
export function asString (format, date) {
    if (typeof format !== 'string') {
        date = format;
        format = module.exports.ISO8601_FORMAT;
    }
    if (!date) {
        date = module.exports.now();
    }
  
    // Issue # 14 - Per ISO8601 standard, the time string should be local time
    // with timezone info.
    // See https://en.wikipedia.org/wiki/ISO_8601 section "Time offsets from UTC"
  
    const vDay = addZero(date.getDate());
    const vMonth = addZero(date.getMonth() + 1);
    const vYearLong = addZero(date.getFullYear());
    const vYearShort = addZero(vYearLong.substring(2, 4));
    const vYear = format.indexOf('yyyy') > -1 ? vYearLong : vYearShort;
    const vHour = addZero(date.getHours());
    const vMinute = addZero(date.getMinutes());
    const vSecond = addZero(date.getSeconds());
    const vMillisecond = padWithZeros(date.getMilliseconds(), 3);
    const vTimeZone = offset(date.getTimezoneOffset());
    const formatted = format
        .replace(/dd/g, vDay)
        .replace(/MM/g, vMonth)
        .replace(/y{1,4}/g, vYear)
        .replace(/hh/g, vHour)
        .replace(/mm/g, vMinute)
        .replace(/ss/g, vSecond)
        .replace(/SSS/g, vMillisecond)
        .replace(/O/g, vTimeZone);
    return formatted;
}
  
function setDatePart (date, part, value, local) {
    date[`set${local ? '' : 'UTC'}${part}`](value);
}
  
function extractDateParts (pattern, str, missingValuesDate) {
    // Javascript Date object doesn't support custom timezone.  Sets all felds as
    // GMT based to begin with.  If the timezone offset is provided, then adjust
    // it using provided timezone, otherwise, adjust it with the system timezone.
    const local = pattern.indexOf('O') < 0;
    const matchers = [
        {
            pattern: /y{1,4}/,
            regexp: '\\d{1,4}',
            fn (date, value) {
                setDatePart(date, 'FullYear', value, local);
            }
        },
        {
            pattern: /MM/,
            regexp: '\\d{1,2}',
            fn (date, value) {
                setDatePart(date, 'Month', (value - 1), local);
            }
        },
        {
            pattern: /dd/,
            regexp: '\\d{1,2}',
            fn (date, value) {
                setDatePart(date, 'Date', value, local);
            }
        },
        {
            pattern: /hh/,
            regexp: '\\d{1,2}',
            fn (date, value) {
                setDatePart(date, 'Hours', value, local);
            }
        },
        {
            pattern: /mm/,
            regexp: '\\d\\d',
            fn (date, value) {
                setDatePart(date, 'Minutes', value, local);
            }
        },
        {
            pattern: /ss/,
            regexp: '\\d\\d',
            fn (date, value) {
                setDatePart(date, 'Seconds', value, local);
            }
        },
        {
            pattern: /SSS/,
            regexp: '\\d\\d\\d',
            fn (date, value) {
                setDatePart(date, 'Milliseconds', value, local);
            }
        },
        {
            pattern: /O/,
            regexp: '[+-]\\d{3,4}|Z',
            fn (date, value) {
                if (value === 'Z') {
                    value = 0;
                }
                const offset = Math.abs(value);
                const timezoneOffset = (value > 0 ? -1 :  1 ) * ((offset % 100) + Math.floor(offset / 100) * 60);
                date.setUTCMinutes(date.getUTCMinutes() + timezoneOffset);
            }
        }
    ];
  
    const parsedPattern = matchers.reduce(
        (p, m) => {
            if (m.pattern.test(p.regexp)) {
                m.index = p.regexp.match(m.pattern).index;
                p.regexp = p.regexp.replace(m.pattern, `(${m.regexp})`);
            } else {
                m.index = -1;
            }
            return p;
        },
        { regexp: pattern, index: [] }
    );
  
    const dateFns = matchers.filter((m) => {
        return m.index > -1;
    });
    dateFns.sort((a, b) => {
        return a.index - b.index;
    });
  
    const matcher = new RegExp(parsedPattern.regexp);
    const matches = matcher.exec(str);
    if (matches) {
        const date = missingValuesDate || now();
        date.setMonth(0);
        date.setDate(1);
        date.setHours(0);
        date.setMinutes(0);
        date.setSeconds(0);
        dateFns.forEach((f, i) => {
            f.fn(date, matches[i + 1]);
        });
  
        return date;
    }
  
    throw new Error(
        `String '${str}' could not be parsed as '${pattern}'`
    );
}
  
export function parse (pattern, str, missingValuesDate) {
    if (!pattern) {
        throw new Error('pattern must be supplied');
    }
  
    return extractDateParts(pattern, str, missingValuesDate);
}
  
/**
   * Used for testing - replace this function with a fixed date.
   */
function now () {
    return new Date();
}

